package com.unitd.configclient;

import com.alibaba.fastjson.JSON;
import com.unitd.configclient.environment.EnvironmentChangeEvent;
import com.unitd.configclient.environment.EnvironmentChangeListener;
import com.unitd.configclient.hanlder.IConfigChangeHanlder;
import com.unitd.configclient.listener.IConfigChangeListener;
import com.unitd.configclient.listener.impl.HttpConfigChangeListener;
import com.unitd.configclient.listener.impl.ZkConfigChangeListener;
import com.unitd.frame.comm.cluster.NodeNameHolder;
import com.unitd.frame.comm.utils.SimpleCryptUtils;
import com.unitd.frame.comm.utils.StringUtils;
import com.unitd.frame.comm.utils.encrypt.RSA;
import com.unitd.frame.comm.utils.http.HttpRequestEntity;
import com.unitd.frame.comm.utils.http.HttpResponseEntity;
import com.unitd.frame.comm.utils.http.HttpUtils;
import com.unitd.frame.spring.SpringInstanceFactory;
import com.unitd.frame.spring.helper.EnvironmentHelper;
import com.unitd.frame.spring.prop.ResourceUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import java.security.PrivateKey;
import java.util.*;
import java.util.Map.Entry;

/**
 * @desc 配置中心上线文信息对象
 * @filename ConfigcenterContext.java
 * @copyright www.pactera.com.cn
 * @author Hudan
 * @version 1.0
 * @date 2018/1/18
 */
public class ConfigcenterContext {

	private final static Logger logger = LoggerFactory.getLogger(ConfigcenterContext.class);

	/** 配置中心上下文对象 */
	private static ConfigcenterContext instance = new ConfigcenterContext();
	private static final String MANAGER_PROPERTY_SOURCE = "manager";

	/** 分布式状态下, 当前应用节点 */
	private final String nodeId = NodeNameHolder.getNodeId();
	/** 是否时springboot项目 */
	private boolean isSpringboot;
	/** 当前应用的应用名称 */
	private String app;
	/** 当前应用的运行环境(dev/test/prep/prod : 开发/测试/预发布/生产) */
	private String env;
	/** 配置文件版本 */
	private String version;
	/** 是否启用配置中心,默认:true */
	private Boolean remoteEnabled;
	/** 配置中心服务端服务URL,多个服务地址之间可以使用,|; 三者任意一个作为分隔符 */
	private String[] apiBaseUrls;
	/** 配置中心客户端与服务端的同步间隔,同步方式为:http时生效(单位:秒) */
	private int syncIntervalSeconds = 90;
	/** RSA 加密私钥 */
	private PrivateKey rsaPrivateKey;
	/** 解密密钥 */
	private String secret;
	/** DES 加密是,加密数据前缀 */
	private static final String DES_PREFIX = "{Cipher}";
	/** RSA 加密是,加密数据前缀 */
	private static final String RSA_PREFIX = "{Cipher:RSA}";
	/** 读取属性文件的属性时,给属性增加响应的前缀 */
	private static final String PLACEHOLDER_PREFIX = "${";
	/** 读取属性文件的属性时,给属性增加响应的后缀 */
	private static final String PLACEHOLDER_SUFFIX = "}";

	/** 是否启用远程配置覆盖本地配置,默认本地配置优先 */
	private boolean remoteFirst = false;
	/** 配置文件改动监听器 */
	private IConfigChangeListener configChangeListener;
	/** 配置文件改动处理器 */
	private List<IConfigChangeHanlder> configChangeHanlders;
	/** 属性配置信息状态 */
	private ConfigStatus status;

	public enum ConfigStatus {
		/** 是否已进行过初始化  */
		INITED,
		/** 已获取 */
		FETCHED,
		/** 已下载 */
		UPLOAED
	}

	private ConfigcenterContext() {
	}

	public static ConfigcenterContext getInstance() {
		return instance;
	}

	/**
	 * @desc 初始化方法, 获取当前配置中心客户端应用的属性配置信息
	 * 普通的项目中将配置信息添加在confcenter.properties中,springboot项目则将配置信息添加在application.properties文件中
	 * @param isSpringboot 是否是springboot项目
	 */
	public void init(boolean isSpringboot) {
		// 是否时springboot项目
		this.isSpringboot = isSpringboot;

		// 需要调用的配置中心客户端的应用名称,springboot项目,默认通过spring.application.name获取项目名称
		String defaultAppName = getValue("spring.application.name");
		app = getValue("unitd.configcenter.appName", defaultAppName);
		Validate.notBlank(app, "[unitd.configcenter.appName] is required");

		// 是否启用配置中心,默认:true,如果不启用,则不需要与配置中心进行交互,直接退出
		if (remoteEnabled == null)
			remoteEnabled = Boolean.parseBoolean(getValue("unitd.configcenter.enabled", "true"));
		if (!remoteEnabled) return;

		// 针对多种启动环境的spring配置,springboot项目,默认通过spring.profiles.active获取当前运行环境
		String defaultEnv = getValue("spring.profiles.active");
		env = getValue("unitd.configcenter.profile", defaultEnv);
		Validate.notBlank(env, "[unitd.configcenter.profile] is required");

		// 配置中心服务地址
		setApiBaseUrl(getValue("unitd.configcenter.base.url"));
		version = getValue("unitd.configcenter.version", "0.0.0");

		// 配置中心客户端与服务端的同步间隔,同步方式为:http时生效
		syncIntervalSeconds = ResourceUtils.getInt("unitd.configcenter.sync-interval-seconds", 90);
		System.out.println(String.format("\n=====configcenter=====\nappName:%s\nenv:%s\nversion:%s\nremoteEnabled:%s\n=====configcenter=====", app, env, version, remoteEnabled));

		// 本地与配置中心交互的密钥文件/证书的存储路径
		String location = StringUtils.trimToNull(ResourceUtils.getProperty("unitd.configcenter.encrypt-keyStore-location"));
		// 密钥库类型
		String storeType = ResourceUtils.getProperty("unitd.configcenter.encrypt-keyStore-type", "JCEKS");
		// 密钥文件的加密密码
		String storePass = ResourceUtils.getProperty("unitd.configcenter.encrypt-keyStore-password");
		// 密钥文件的别名
		String alias = ResourceUtils.getProperty("unitd.configcenter.encrypt-keyStore-alias");
		// 密钥文件的解密密码
		String keyPass = ResourceUtils.getProperty("unitd.configcenter.encrypt-keyStore-keyPassword", storePass);
		try {
			if (StringUtils.isNotBlank(location)) {
				if (location.toLowerCase().startsWith("classpath")) {
					Resource resource = new ClassPathResource(location.substring(location.indexOf(":") + 1));
					location = resource.getFile().getAbsolutePath();
				}
				rsaPrivateKey = RSA.loadPrivateKeyFromKeyStore(location, alias, storeType, storePass, keyPass);
			}
		} catch (Exception e) {
			System.out.println("load private key:" + location);
			e.printStackTrace();
		}
		// 初始化后说明已读取了配置信息,则配置信息对象状态应更改为已初始化过
		status = ConfigStatus.INITED;
	}

	/**
	 * @desc 启动服务的时候, 从配置中心服务端获取初始配置信息
	 * @param properties 属性配置信息
	 * @param first 是否启用远程配置覆盖本地配置,默认本地配置优先
	 */
	public void syncConfigToServer(Properties properties, boolean first) {

		// 如果没有启用配置中心, 则不需要与配置中心服务器进行同步
		if (!remoteEnabled) return;

		if (status.equals(ConfigStatus.INITED)) return;

		// 获取配置中心客户端与服务端的同步方式,默认:http
		String syncType = properties.getProperty("unitd.configcenter.sync-type");
		List<String> sortKeys = new ArrayList<>();
		Map<String, String> params = new HashMap<>(8);
		params.put("nodeId", nodeId);
		params.put("appName", app);
		params.put("env", env);
		params.put("version", version);
		params.put("springboot", String.valueOf(isSpringboot));
		params.put("syncIntervalSeconds", String.valueOf(syncIntervalSeconds));
		if(!StringUtils.isNull(syncType)) {
			params.put("syncType", syncType);
		}
		params.put("serverip", ServerEnvUtils.getServerIpAddr());
		params.put("serverport", ServerEnvUtils.getServerPort());

		Set<Entry<Object, Object>> entrySet = properties.entrySet();
		Iterator iterator = entrySet.iterator();

		String key;
		while(iterator.hasNext()) {
			Entry entry = (Entry)iterator.next();
			key = entry.getKey().toString();
			String value = entry.getValue().toString();
			if (value.contains("${")) {
				value = this.setReplaceHolderRefValue(properties, key, value);
			}

			params.put(key, this.hideSensitive(key, value));
			sortKeys.add(key);
		}

		if (first) {
			Collections.sort(sortKeys);
			System.out.println("==================final config list start==================");
			for (String sortKey : sortKeys) {
				System.out.println(String.format("%s = %s", sortKey, params.get(sortKey)));
			}
			System.out.println("==================final config list end====================");

			// 注册配置信息监听服务
			registerListener(syncType);
		} else {
			// 通过springCloud的属性获取当前服务所属的ip地址信息
			String serverIp = EnvironmentHelper.getProperty("spring.cloud.client.ipAddress");
			if (StringUtils.isNotBlank(serverIp)) {
				params.put("serverip", serverIp);
			}
		}

		// 向配置中心服务端发送获取当前应用的有效属性配置信息完成,并告知服务端更新相应的状态
		for (String apiBaseUrl : apiBaseUrls) {
			String url = apiBaseUrl + "/api/notify_final_config";
			logger.info("syncConfigToServer,url:" + url);
			HttpResponseEntity responseEntity = HttpUtils.doPost(url, JSON.toJSONString(params), HttpUtils.DEFAULT_CHARSET);
			if(responseEntity.isSuccessed()){
				logger.info("syncConfigToServer[{}] Ok",url);
			}else{
				logger.warn("syncConfigToServer[{}] error",url);
			}
		}

		status = ConfigStatus.UPLOAED;
	}

	/**
	 * @desc 更新属性配置信息
	 * @param updateConfig 需要更新的属性配置信息
	 */
	public synchronized void updateConfig(Map<String, Object> updateConfig) {
		if (!updateConfig.isEmpty()) {
			Set<String> keySet = updateConfig.keySet();
			for (String key : keySet) {
				String oldValue = ResourceUtils.getProperty(key);
				ResourceUtils.add(key, decodeEncryptIfRequire(updateConfig.get(key)).toString());

				StandardEnvironment environment = SpringInstanceFactory.getInstance(StandardEnvironment.class);
				MutablePropertySources propertySources = environment.getPropertySources();

				MapPropertySource source;
				synchronized (propertySources) {
					if (!propertySources.contains(MANAGER_PROPERTY_SOURCE)) {
						source = new MapPropertySource(MANAGER_PROPERTY_SOURCE, new LinkedHashMap<>());
						environment.getPropertySources().addFirst(source);
					} else {
						source = (MapPropertySource) propertySources.get(MANAGER_PROPERTY_SOURCE);
					}
				}

				Map<String, Object> map = source.getSource();
				Properties properties = new Properties();
				properties.putAll(map);
				properties.putAll(updateConfig);
				propertySources.replace(source.getName(), new PropertiesPropertySource(source.getName(), properties));

				// 发布更改事件
				if (SpringInstanceFactory.getInstance(EnvironmentChangeListener.class) != null) {
					try {
						SpringInstanceFactory.getSpringInstanceProvider().getApplicationContext().publishEvent(new EnvironmentChangeEvent(updateConfig.keySet()));
					} catch (Exception e) {
						e.printStackTrace();
					}
				}

				if (configChangeHanlders == null) {
					configChangeHanlders = new ArrayList<>();
					Map<String, IConfigChangeHanlder> interfaces = SpringInstanceFactory.getSpringInstanceProvider().getInterfaces(IConfigChangeHanlder.class);
					if (interfaces != null) {
						configChangeHanlders.addAll(interfaces.values());
					}
				}

				for (IConfigChangeHanlder hander : configChangeHanlders) {
					try {
						hander.onConfigChanged(updateConfig);
						logger.info("invoke {}.onConfigChanged successed!", hander.getClass().getName());
					} catch (Exception e) {
						logger.warn("invoke {}.onConfigChanged error,msg:{}", hander.getClass().getName(), e.getMessage());
					}
				}

				logger.info("Config [{}] Change,oldValue:{},newValue:{}", key, oldValue, updateConfig.get(key));
			}
		}
	}

	/**
	 * @desc 与配置中心服务端进行心跳检测
	 * @param pingUrl 配置中心服务端或者zookeeper服务URL
	 * @param retry 重试次数
	 * @return 客户端能否ping通服务端
	 */
	public boolean pingCcServer(String pingUrl, int retry) {
		boolean result = false;
		try {
			System.out.println("pingCcServer ,retry:" + retry);
			result = HttpUtils.doGet(pingUrl, HttpRequestEntity.create().connectTimeout(2000).readTimeout(2000)).isSuccessed();
		} catch (Exception e) {
		}
		if (retry == 0) return false;

		// 没有与配置中心服务端获取心跳,按照指定的最大重试次数继续重试进行心跳检测
		if (!result) {
			try {
				Thread.sleep(1500);
			} catch (Exception e) {
			}
			return pingCcServer(pingUrl, --retry);
		}

		return result;
	}

	/**
	 * @desc 关闭当前服务,断开与配置中心或者zookeeper服务的连接,释放连接
	 */
	public void close() {
		configChangeListener.unRegister();
	}

	/**
	 * @desc 合并远程属性配置信息到本地
	 * @param properties 属性配置
	 */
	public void mergeRemoteProperties(Properties properties) {
		// 获取远程配置中心的所有有效属性配置信息
		Properties remoteProperties = getAllRemoteProperties();
		if (remoteProperties != null) {
			// 合并属性
			Set<Entry<Object, Object>> entrySet = remoteProperties.entrySet();
			for (Entry<Object, Object> entry : entrySet) {
				// 本地配置优先
				if (!isRemoteFirst() && properties.containsKey(entry.getKey())) {
					logger.info("config[{}] exists in location,skip~", entry.getKey());
					continue;
				}
				properties.setProperty(entry.getKey().toString(), entry.getValue().toString());
			}
		}

		// 替换本地变量占位符
		Set<Entry<Object, Object>> entrySet = properties.entrySet();
		for (Entry<Object, Object> entry : entrySet) {
			String key = entry.getKey().toString();
			String value = entry.getValue().toString();

			if (value.contains(PLACEHOLDER_PREFIX)) {
				value = setReplaceHolderRefValue(properties, key, value);
				properties.setProperty(key, value);
			}
			ResourceUtils.add(key, value);
		}
	}

	/**
	 * @param apiBaseUrl 配置中心服务地址URL
	 * @desc 获取配置中心服务地址
	 */
	private void setApiBaseUrl(String apiBaseUrl) {
		// 验证配置中心服务端URL参数是否已配置
		Validate.notBlank(apiBaseUrl, "[unitd.configcenter.base.url] is required");

		String[] urls = apiBaseUrl.split(",|;");
		this.apiBaseUrls = new String[urls.length];

		for (int i = 0; i < urls.length; i++) {
			if (urls[i].endsWith("/")) {
				this.apiBaseUrls[i] = urls[i].substring(0, urls[i].length() - 1);
			} else {
				this.apiBaseUrls[i] = urls[i];
			}
		}
	}

	/**
	 * @desc 获取远程配置中心的所有有效属性配置信息
	 * @return Properties
	 */
	private Properties getAllRemoteProperties() {
		if (!remoteEnabled) return null;

		Properties properties = new Properties();
		Map<String, Object> map = fetchConfigFromServer(2);
		if (map == null) {
			throw new RuntimeException("fetch remote config error!");
		}

		// DES解密密匙
		secret = Objects.toString(map.remove("unitd.configcenter.encrypt-secret"), null);
		// 是否启用远程配置覆盖本地配置,默认本地配置优先
		remoteFirst = Boolean.parseBoolean(Objects.toString(map.remove("unitd.configcenter.remote-config-first"), "false"));

		Set<String> keys = map.keySet();
		for (String key : keys) {
			Object value = decodeEncryptIfRequire(map.get(key));
			properties.put(key, value);
		}

		status = ConfigStatus.FETCHED;

		return properties;
	}

	/**
	 * @desc 根据属性配置对象信息给有占位符的指定的key设置相应的值
	 * @param properties 属性配置对象信息
	 * @param key 指定的key
	 * @param value 占位符信息
	 * @return 给占位符设置的值
	 */
	private String setReplaceHolderRefValue(Properties properties, String key, String value) {

		String[] segments = value.split("\\$\\{");
		String seg;

		StringBuilder result = new StringBuilder();
		for (String segment : segments) {
			seg = StringUtils.trimToNull(segment);
			if (StringUtils.isBlank(seg)) continue;

			if (seg.contains(PLACEHOLDER_SUFFIX)) {
				String refKey = seg.substring(0, seg.indexOf(PLACEHOLDER_SUFFIX)).trim();
				// 其他非${}的占位符如：{{host}}
				String withBraceString = null;
				if (seg.contains("{")) {
					withBraceString = seg.substring(seg.indexOf(PLACEHOLDER_SUFFIX) + 1);
				}

				// 如果包含默认值，如：${host:127.0.0.1}
				String orginKey = refKey;
				if (refKey.contains(":")) {
					refKey = refKey.split(":")[0];
				}

				String refValue = properties.getProperty(refKey);

				if (StringUtils.isNotBlank(refValue) && refValue.contains(PLACEHOLDER_PREFIX)) {
					// TODO 避免多层嵌套死循环
					// refValue = setReplaceHolderRefValue(properties, refKey, refValue);
					String subRefKey = refValue.replace(PLACEHOLDER_PREFIX, "").replace(PLACEHOLDER_SUFFIX, "");
					refValue = properties.getProperty(subRefKey);
				}

				if (StringUtils.isBlank(refValue)) {
					refValue = System.getProperty(refKey);
				}
				if (StringUtils.isBlank(refValue)) {
					refValue = PLACEHOLDER_PREFIX + orginKey + PLACEHOLDER_SUFFIX;
				}
				result.append(refValue);

				if (withBraceString != null) {
					result.append(withBraceString);
				} else {
					String[] segments2 = seg.split("\\}");
					if (segments2.length == 2) {
						result.append(segments2[1]);
					}
				}
			} else {
				result.append(seg);
			}
		}

		properties.put(key, result.toString());
		return result.toString();
	}

	/**
	 * @desc 从配置中心服务端获取当前应用所属的配置信息
	 * @param retry 重试次数
	 * @return 属性配置信息
	 */
	private Map<String, Object> fetchConfigFromServer(int retry) {
		if (retry == 0) return null;
		Map<String, Object> result = null;
		String errorMsg;

		for (String apiBaseUrl : apiBaseUrls) {
			String url = String.format("%s/api/fetch_all_configs?appName=%s&env=%s&version=%s", apiBaseUrl, app, env, version);
			System.out.println("fetch configs url:" + url);

			HttpResponseEntity response = HttpUtils.doGet(url);
			if (response.isSuccessed()) {
				String jsonString = response.getBody();
				result = JSON.parseObject(jsonString, Map.class);
				if (result.containsKey("code")) {
					errorMsg = result.get("msg").toString();
					System.out.println("fetch error:" + errorMsg);
					result = null;
				} else {
					break;
				}
			}
		}

		// 如果没有获取到,再重试去获取,重试次数跟传递的重试次数有关
		if (result == null) {
			result = fetchConfigFromServer(--retry);
		}
		return result;
	}

	/**
	 * @desc 根据配置中心客户端与服务端的同步方式, 注册相应的监听器
	 * @param syncType 配置中心客户端与服务端的同步方式
	 */
	private void registerListener(String syncType) {
		// 通过zookeeper,保持配置中心客户端与服务端的监听
		if ("zookeeper".equals(syncType)) {
			String zkServers = ResourceUtils.getProperty("unitd.configcenter.sync-zk-servers");
			if (StringUtils.isBlank(zkServers)) {
				throw new RuntimeException("config[unitd.configcenter.sync-zk-servers] is required for syncType [zookeeper] ");
			}
			configChangeListener = new ZkConfigChangeListener(zkServers);
		} else {
			// 通过http,保持配置中心客户端与服务端的监听
			configChangeListener = new HttpConfigChangeListener();
		}
		configChangeListener.register(this);
	}

	/**
	 * @desc 根据情况,使用RSA私钥解密RSA加密数据
	 * @param data 需要解密的数据
	 * @return 解密后的对象
	 */
	private Object decodeEncryptIfRequire(Object data) {
		if (data.toString().startsWith(RSA_PREFIX)) {
			if (rsaPrivateKey == null) {
				throw new RuntimeException("configcenter [rsaPrivateKey] is required");
			}
			data = data.toString().replace(RSA_PREFIX, "");

			return RSA.decryptByPrivateKey(rsaPrivateKey,  data.toString().getBytes());
		} else if (data.toString().startsWith(DES_PREFIX)) {
			if (StringUtils.isBlank(secret)) {
				throw new RuntimeException("configcenter [unitd.configcenter.encrypt-secret] is required");
			}
			data = data.toString().replace(DES_PREFIX, "");
			return SimpleCryptUtils.decrypt(secret, data.toString());
		}
		return data;
	}

	/** 需要进行脱敏处理的数据的key */
	List<String> sensitiveKeys = new ArrayList<>(Arrays.asList("pass", "key", "secret", "token", "credentials"));
	/**
	 * @desc 隐藏敏感的加密数据信息(脱敏处理)
	 * @param key 需要隐藏的数据的key
	 * @param orign 实际的值
	 * @return 进行隐藏后的数据
	 */
	private String hideSensitive(String key, String orign) {
		boolean is = false;
		for (String k : sensitiveKeys) {
			if (is = key.toLowerCase().contains(k)) break;
		}
		int length = orign.length();
		if (is && length > 1) return orign.substring(0, length / 2).concat("****");
		return orign;
	}

	/**
	 * @desc 根据key,获取属性配置信息相应的值
	 * @param key 属性配置的key
	 * @return 属性值
	 */
	private String getValue(String key) {
		return getValue(key, null);
	}

	/**
	 * @desc 根据key,获取属性配置信息相应的值, 如果值不存在,则取默认值
	 * @param key 属性配置的key
	 * @param defVal 默认值
	 * @return 属性值
	 */
	private String getValue(String key, String defVal) {
		//jvm 启动参数优先
		String value = System.getProperty(key);
		if (StringUtils.isNotBlank(value)) return value;
		value = StringUtils.trimToNull(ResourceUtils.getProperty(key, defVal));
		if (StringUtils.isNotBlank(value)) {
			if (value.startsWith(PLACEHOLDER_PREFIX)) {
				String refKey = value.substring(2, value.length() - 1).trim();
				value = ResourceUtils.getProperty(refKey);
			}
		}
		return value;
	}

	public String getNodeId() {
		return nodeId;
	}

	public boolean isSpringboot() {
		return isSpringboot;
	}

	public String getApp() {
		return app;
	}

	public String getEnv() {
		return env;
	}

	public String getVersion() {
		return version;
	}

	public boolean isRemoteEnabled() {
		return remoteEnabled;
	}

	public void setRemoteEnabled(boolean remoteEnabled) {
		this.remoteEnabled = remoteEnabled;
	}

	public String[] getApiBaseUrls() {
		return apiBaseUrls;
	}

	public int getSyncIntervalSeconds() {
		return syncIntervalSeconds;
	}

	public String getSecret() {
		return secret;
	}

	public boolean isRemoteFirst() {
		return remoteFirst;
	}
}